<meta http-equiv="content-type" content="text/html; charset=UTF-8">

<link href="css/markdown.css" rel="stylesheet" />

<link href="css/prettify.css" rel="stylesheet" />

<script src="http://apps.bdimg.com/libs/jquery/2.0.3/jquery.min.js"></script>

<script src="js/prettify.js"></script>  

<h1 id="koa">KOA</h1>
<blockquote>
<p>类似express的框架，由 Express 同一作者 koa，致力于成为一个更小、更健壮、更富有表现力的 Web 框架。
支持async await es7语法，可以免除重复繁琐的回调函数嵌套，并极大地提升常用错误处理效率。
Koa 不在内核方法中绑定任何中间件，它仅仅提供了一个轻量优雅的函数库，使得编写 Web 应用变得得心应手。</p>
</blockquote>
<h2 id="-">目录</h2>
<ul>
<li><a href="#网站">网站</a></li>
<li><a href="#更多资源">更多资源</a></li>
<li><a href="#开始使用">开始使用</a></li>
<li><a href="#项目配置">项目配置</a></li>
<li><a href="#项目配置文件">项目配置文件</a></li>
<li><a href="#示例代码">示例代码</a></li>
<li><a href="#静态文件服务">静态文件服务</a></li>
<li><a href="#路由">路由</a><ul>
<li><a href="#创建路由">创建路由</a></li>
<li><a href="#示例代码">示例代码</a></li>
</ul>
</li>
<li><a href="#jade模板">jade模板</a></li>
<li><a href="#参数设置">参数设置</a><ul>
<li><a href="#参数存放">参数存放</a></li>
<li><a href="#configappjs-参数文件举例">config/app.js 参数文件举例</a></li>
<li><a href="#配置文件使用示例">配置文件使用示例</a></li>
</ul>
</li>
<li><a href="#用户逻辑层封装">用户逻辑层封装</a></li>
<li><a href="#mongodb-数据库">MongoDB 数据库</a><ul>
<li><a href="#model-中存放数据模型">model 中存放数据模型</a></li>
</ul>
</li>
<li><a href="#日志">日志</a></li>
<li><a href="#部署运行">部署运行</a><ul>
<li><a href="#无中断重启">无中断重启</a></li>
<li><a href="#级联概念">级联概念</a></li>
<li><a href="#代码示例">代码示例：</a></li>
<li><a href="#示例解析">示例解析</a></li>
<li><a href="#级联调用形象示意图">级联调用形象示意图</a></li>
</ul>
</li>
<li><a href="#代码解析">代码解析</a><ul>
<li><a href="#applisten">app.listen(...)</a></li>
<li><a href="#appcallback">app.callback()</a></li>
<li><a href="#appusefunction">app.use(function)</a></li>
<li><a href="#appkeys">app.keys=</a></li>
</ul>
</li>
<li><a href="#中间件">中间件</a></li>
</ul>
<p><div id='网站'/></p>
<h2 id="-">网站</h2>
<p><strong>官方网站</strong>: <a href="http://koajs.com">http://koajs.com</a><br><strong>GitHub</strong>：<a href="https://github.com/koajs/koa">https://github.com/koajs/koa</a>
<strong>中文文档</strong>：<a href="http://koajs.cn/">http://koajs.cn/</a><br><strong>KOA实战</strong>:<a href="http://book.apebook.org/minghe/koa-action/index.html">http://book.apebook.org/minghe/koa-action/index.html</a>
<strong>示例</strong>: <a href="https://github.com/koajs/examples">https://github.com/koajs/examples</a></p>
<p><div id='更多资源'/></p>
<h2 id="-">更多资源</h2>
<ul>
<li><a href="https://github.com/koajs/koa/tree/master/docs/api/index.md">接口文档</a></li>
<li><a href="https://koajs.github.io/badgeboard">官方模块列表</a></li>
<li><a href="https://github.com/koajs/koa/wiki">中间件列表</a></li>
<li><a href="https://github.com/koajs/koa/wiki">Wiki</a></li>
<li><a href="http://reddit.com/r/koajs">Reddit Community</a></li>
<li><a href="https://groups.google.com/forum/#!forum/koajs">邮件列表</a></li>
<li><a href="https://github.com/koajs/koa/tree/master/docs/guide.md">使用指南</a></li>
<li><a href="https://github.com/koajs/koa/tree/master/docs/faq.md">FAQ</a></li>
<li><a href="https://github.com/koajs/kick-off-koa">Kick-Off-Koa</a> 
An intro to koa via a set of self-guided workshops.</li>
<li><a href="https://github.com/koajs/workshop">Workshop</a> 
A workshop to learn the basics of koa, Express&#39; spiritual successor.</li>
<li><a href="http://knowthen.com/episode-3-koajs-quickstart-guide/">Introduction Screencast</a>
An introduction to installing and getting started with Koa</li>
</ul>
<p><div id='开始使用'/></p>
<h2 id="-">开始使用</h2>
<ul>
<li>安装git，<a href="https://github.com/nydl/devnote/blob/master/git.md">git安装请参见 git笔记</a>
一般安默认选项提示安装，最后选择路径时，选择最后一个。<br>安装完毕 命令行中 输入 <code>git --version</code> 可以看到 安装的 版本，如果看不到，说明安装有问题！<br>如果已安装，无需重复安装。</li>
<li><p>克隆 koastart项目
进入命令行（windows系统win+r键），切换到 d 盘，在d盘创建一个 prj 目录，用了存放项目文件，操作示意：</p>
<pre><code class="lang-js">d:  
cd /  
mkdir prj   
cd prj   
// 将项目克隆到 d盘 prj目录下的 koastart 路径！
clone https://github.com/nydl/koastart.git koastart
</code></pre>
</li>
<li>安装最新的 node.js，如果已经安装，跳过！
测试：在命令行下面 输入 <code>node -v</code>，如果能看到node的版本，说明安装正确。<br>node 版本更新非常快，2015年5月18日的最新版本是 6.10，尽量使用最新的稳定版本。</li>
<li>npm镜像：npm 修改npm服务器为淘宝镜像服务器，加快安装速度，这个只需执行一次，之前执行过了，跳过！    <ul>
<li>npm config set registry <a href="https://registry.npm.taobao.org">https://registry.npm.taobao.org</a>  </li>
<li>npm config set disturl <a href="https://npm.taobao.org/dist">https://npm.taobao.org/dist</a></li>
</ul>
</li>
<li>安装第三方库：<code>npm i</code>，注意，一定要在 koastart 这个路径执行！！！</li>
<li>编译代码：<code>npm run build</code>，注意，一定要在 koastart这个路径执行！！！
该命令生成 在 lib 目录下生成可执行代码！</li>
<li>运行文件：<code>node lib/app</code>，提示 koa 服务运行，服务端口3000，说明运行成功！
注意，一定要在 koastart 这个路径执行！！！</li>
<li>通过浏览器访问服务：<a href="http://127.0.0.1:3000">http://127.0.0.1:3000</a> 或 <a href="http://127.0.0.1:3000/koa.html，">http://127.0.0.1:3000/koa.html，</a><br>如果能看到内容，恭喜你，koa web服务搭建成功，可以开始你的成长之旅！</li>
<li>通过 <code>ipconfig</code> 指令查看本机ip地址，手机与计算机在同一网络时，通过手机也可以浏览你的网站
相当于将你的计算机作为服务器，在开发、调试手机、微信页面时，非常方便，不用专门的服务器。</li>
</ul>
<h2 id="-">项目配置</h2>
<ul>
<li>koa 2.0 需要使用 Babel，需要安装 Babel转换器<ul>
<li>使用了es7的语法，如 async、await</li>
</ul>
</li>
<li>强烈推荐使用 web storm 编辑项目</li>
<li>web storm 设置：<ul>
<li>js 版本：JSX Harmoney，最大限度支持新语法</li>
<li>ESLint：Java Script 中开启 ESLint,提供代码编写规范检查</li>
<li>Babel自动转换：使用 js 6后，webstorm会提示 一个 自动转换为 5 的 插件：
File watcher &#39;Babel&#39; is available for thie file. Description:&#39;Transpiles ECMAScript 6 code to ECMAScript 5&#39;
这个插件在你修改文件时，会自动将es6、es7语法文件转换为es5或es6的文件。<br>建议不要使用，会影响编码效率，学习、测试可以使用，涉及多个文件引用时，引用文件如果采用了新语法，会报错。
也可以通过tools手动添加。</li>
</ul>
</li>
<li>js编译：<code>npm run build</code>，npm 会在项目的 package.json 文件中寻找 scripts 区域中的命令。<br>其实npm test和npm start是npm run test和npm run start的简写。事实上，你可以使用npm run来运行scripts里的任何条目。<br>使用npm run的方便之处在于，npm会自动把node_modules/.bin加入$PATH，这样你可以直接运行依赖程序和开发依赖程序，不用全局安装了。 只要npm上的包提供命令行接口，你就可以直接使用它们，方便吧。</li>
<li>webstorm 调试：<ul>
<li>添加调试文件时，使用 babel-node 代替 标准 node
比如 osx 上：
/Users/way/.nvm/versions/node/v5.10.1/bin/node
替换为：
/Users/way/.nvm/versions/node/v5.10.1/bin/babel-node</li>
<li>编译时带&quot;-s&quot;生成 Source Maps，运行js时，可对编译前的代码进行调试！
&quot;build&quot;: &quot;node_modules/.bin/babel src -d lib -s&quot;,</li>
</ul>
</li>
<li>如果浏览器提示错误，在浏览器上先连一下调试端口，然后打开web端口，即可触发调试</li>
</ul>
<h2 id="-">项目配置文件</h2>
<blockquote>
<p>所有项目配置文件已经在 clone时自动下载到你的计算机，无需另外设置，列在这里，有兴趣的同学可以看看。</p>
</blockquote>
<ul>
<li><p>package.json 项目配置参考文件：</p>
<pre><code class="lang-js">{
  &quot;name&quot;: &quot;koastart&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;description&quot;: &quot;&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;lint&quot;: &quot;node_modules/.bin/eslint ./src&quot;,
    &quot;test&quot;: &quot;node_modules/.bin/eslint ./src&quot;,
    &quot;build&quot;: &quot;node_modules/.bin/babel src -d lib&quot;,
    &quot;prepublish&quot;: &quot;node_modules/.bin/babel src -d lib&quot;
  },
  &quot;author&quot;: &quot;&quot;,
  &quot;license&quot;: &quot;MIT&quot;,
  &quot;private&quot;: true,
  &quot;dependencies&quot;: {
    &quot;filesize-parser&quot;: &quot;^1.3.1&quot;,
    &quot;fs-promise&quot;: &quot;^0.5.0&quot;,
    &quot;jsrsasign&quot;: &quot;^5.0.11&quot;,
    &quot;koa&quot;: &quot;^2.0.0&quot;,
    &quot;koa-bodyparser&quot;: &quot;^3.0.0&quot;,
    &quot;koa-router&quot;: &quot;^7.0.0&quot;,
    &quot;koa-send&quot;: &quot;^3.2.0&quot;,
    &quot;mysql&quot;: &quot;^2.10.2&quot;,
    &quot;nodemailer&quot;: &quot;^2.3.0&quot;,
    &quot;progress&quot;: &quot;^1.1.8&quot;,
    &quot;qiniu&quot;: &quot;^6.1.9&quot;,
    &quot;read&quot;: &quot;^1.0.7&quot;,
    &quot;request&quot;: &quot;^2.69.0&quot;,
    &quot;sequelize&quot;: &quot;^3.19.3&quot;,
    &quot;uid-safe&quot;: &quot;^2.0.0&quot;,
    &quot;umzug&quot;: &quot;^1.9.0&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;babel-cli&quot;: &quot;^6.8.0&quot;,
    &quot;babel-eslint&quot;: &quot;^6.0.4&quot;,
    &quot;babel-plugin-syntax-async-functions&quot;: &quot;^6.5.0&quot;,
    &quot;babel-plugin-syntax-object-rest-spread&quot;: &quot;^6.5.0&quot;,
    &quot;babel-plugin-transform-async-to-generator&quot;: &quot;^6.3.13&quot;,
    &quot;babel-plugin-transform-es2015-arrow-functions&quot;: &quot;^6.5.2&quot;,
    &quot;babel-plugin-transform-es2015-destructuring&quot;: &quot;^6.3.15&quot;,
    &quot;babel-plugin-transform-es2015-modules-commonjs&quot;: &quot;^6.3.16&quot;,
    &quot;babel-plugin-transform-es2015-parameters&quot;: &quot;^6.5.0&quot;,
    &quot;babel-plugin-transform-es2015-spread&quot;: &quot;^6.5.2&quot;,
    &quot;babel-plugin-transform-object-rest-spread&quot;: &quot;^6.5.0&quot;,
    &quot;babel-plugin-transform-strict-mode&quot;: &quot;^6.5.2&quot;,
    &quot;babel-register&quot;: &quot;^6.3.13&quot;,
    &quot;eslint&quot;: &quot;^2.9.0&quot;,
    &quot;eslint-config-airbnb&quot;: &quot;^9.0.1&quot;,
    &quot;eslint-plugin-import&quot;: &quot;^1.8.0&quot;,
    &quot;eslint-plugin-jsx-a11y&quot;: &quot;^1.2.0&quot;,
    &quot;eslint-plugin-react&quot;: &quot;^5.1.1&quot;,
    &quot;sequelize-cli&quot;: &quot;^2.3.1&quot;
  }
}
</code></pre>
</li>
<li><p>.babelrc 转换配置文件</p>
<pre><code class="lang-js">{
  &quot;plugins&quot;: [
    &quot;syntax-object-rest-spread&quot;,
    &quot;syntax-async-functions&quot;,
    &quot;transform-es2015-arrow-functions&quot;,
    &quot;transform-async-to-generator&quot;,
    &quot;transform-es2015-modules-commonjs&quot;,
    &quot;transform-es2015-destructuring&quot;,
    &quot;transform-es2015-spread&quot;,
    &quot;transform-object-rest-spread&quot;,
    &quot;transform-es2015-parameters&quot;,
    &quot;transform-strict-mode&quot;
  ]
}
</code></pre>
</li>
<li><p>.eslintrc 转换配置文件</p>
<pre><code class="lang-js">{
  &quot;extends&quot;: &quot;eslint-config-airbnb/base&quot;,
  &quot;parser&quot;: &quot;babel-eslint&quot;,
  &quot;env&quot;: {
    &quot;browser&quot;: true,
    &quot;node&quot;: true,
    &quot;mocha&quot;: true
  },
  &quot;rules&quot;: {
    // Disable for console/alert
    &quot;no-console&quot;: 0,
    &quot;no-alert&quot;: 0,

    &quot;indent&quot;: [2, 2, {&quot;SwitchCase&quot;: 1}]
  },
  &quot;plugins&quot;: [
    &quot;import&quot;
  ],
  &quot;settings&quot;: {
    &quot;import/parser&quot;: &quot;babel-eslint&quot;,
    &quot;import/resolve&quot;: {
      &quot;moduleDirectory&quot;: [&quot;node_modules&quot;, &quot;src&quot;]
    }
  },
  &quot;globals&quot;: {
    &quot;__DEV__&quot;: true,
    &quot;__OPTION__&quot;: true
  }
}
</code></pre>
</li>
</ul>
<p><div id='示例代码'/>  </p>
<h2 id="-">示例代码</h2>
<ul>
<li><p>Hello World</p>
<pre><code class="lang-js">var koa = require(&#39;koa&#39;);  
var app = koa();

app.use(ctx=&gt;  
  ctx.body = &#39;Hello World&#39;;
);

app.listen(3000);
</code></pre>
</li>
<li><p>插入中间件并等待后续执行</p>
<pre><code class="lang-js">var koa = require(&#39;koa&#39;);
var app = koa();

// logger 中间件
app.use(async (ctx,next)=&gt;{
  const start = new Date();
  await next();
  const ms = new Date - start;
  console.log(`logger: ${ctx.method} ${ctx.url} - ${ms}ms`);
});

// response
app.use(ctx =&gt;{
  ctx.body = &#39;Hello World！&#39;;
});

app.listen(3000);
</code></pre>
</li>
</ul>
<p><div id='日志'/> </p>
<h2 id="-">日志</h2>
<blockquote>
<p>系统内置的 console.log、console.warn、console.error在简单程序时，可以输出控制台，但是在大型系统设计时，console就不够用，需要更强大的日志功能，比如常用的安日期存储日志、分离错误日志、专用日志服务器、可配置的日志级别，在调试时输出所有日志，投产后只输出错误日志等。</p>
</blockquote>
<p>常用日志目前有两种，使用的人都非常多：</p>
<ul>
<li><a href="https://github.com/winstonjs/winston">winston</a>
5000多 star，人气最高，功能强大，使用相对稍复杂</li>
<li><a href="https://github.com/nomiddlename/log4js-node">log4js</a>
1400多 star，使用简单，一般项目足够使用。<br>安装：<code>npm install log4js -S</code></li>
<li><a href="https://github.com/nomiddlename/log4js-node/tree/master/examples">log4js各种示例</a></li>
<li><a href="https://github.com/nomiddlename/log4js-example">log4js完整示例</a></li>
</ul>
<h3 id="log4js-">log4js 功能</h3>
<ul>
<li>彩色控制台输出 stdout or stderr</li>
<li>替代了 console.log functions (optional)</li>
<li>基于大小的文件滚动输出</li>
<li>邮件输出</li>
<li>GELF 输出</li>
<li>hook.io 输出</li>
<li>Loggly appender</li>
<li>Logstash UDP 网络输出</li>
<li>多处理器输出 (多工作进程时有用)</li>
<li>connect/express 服务的标准日志中间件</li>
<li>可设置日志消息的模板</li>
<li>针对不同的日志类别，可设置不同的日志级别 (比如一部分日志级别为 DEBUG, 另外的只输出 ERRORS)</li>
</ul>
<h2 id="log4js-">log4js 使用</h2>
<ul>
<li>跟踪：logger.trace(&#39;Entering cheese testing&#39;);
一般用于详细跟踪执行过程，投产需关闭</li>
<li>调试：logger.debug(&#39;Got cheese.&#39;);
特定部分的调试信息，投产需关闭  </li>
<li>信息：logger.info(&#39;Cheese is Gouda.&#39;);
普通日志信息，提示作用，投产可关闭可打开</li>
<li>告警：logger.warn(&#39;Cheese is quite smelly.&#39;);
告警信息，投产需打开</li>
<li>错误：logger.error(&#39;Cheese is too ripe!&#39;);
错误信息，投产需打开</li>
<li>致命错误：logger.fatal(&#39;Cheese was breeding ground for listeria.&#39;);
会引起崩溃的严重错误，投产需打开</li>
</ul>
<h2 id="log4js-json-">log4js.json 配置文件</h2>
<ul>
<li>appenders 是日志输出数组，可配置多个日志输出</li>
<li>type 是输出类型，有控制台、文件、日期文件等</li>
<li>pattern 日期后缀模板</li>
<li>replaceConsole 替换cosole输出</li>
<li>加入 console 会将 console.log 记录到日志文件</li>
<li>category 类别，实际上就是名称，一般可以不设置。
如果设置了，需要<code>const log = log4js.getLogger(category);</code>使用。<br>如果没有设置，为全局输出，对所有类别均适用，比如 <code>const log = log4js.getLogger(&#39;modualname&#39;);</code>，没有找到匹配的名称，则自动匹配没有设置 category的输出！</li>
<li>level 等级：ALL TRACE DEBUG INFO WARN ERROR FATAL OFF，控制输出级别，设置 ERROR，只输出 ERROR、FATAL、</li>
<li>&quot;[all]&quot; 针对所有没有设置 category 的全局输出的级别代称。</li>
</ul>
<h3 id="log4js-json-">log4js.json配置文件举例：</h3>
<pre><code class="lang-js">{
  &quot;appenders&quot;: [
    {
      &quot;type&quot;:&quot;console&quot;
    },
    {
      &quot;type&quot;: &quot;file&quot;,
      &quot;filename&quot;: &quot;log/all.log&quot;,
      &quot;maxLogSize&quot;: 104857600,
      &quot;numBackups&quot;: 3
    },
    {
      &quot;type&quot;: &quot;logLevelFilter&quot;,
      &quot;level&quot;: &quot;ERROR&quot;,
      &quot;appender&quot;: {
        &quot;type&quot;: &quot;file&quot;,
        &quot;filename&quot;: &quot;log/err.log&quot;,
        &quot;maxLogSize&quot;: 104857600,
        &quot;numBackups&quot;: 3
      }
    },
    {
      &quot;type&quot;: &quot;logLevelFilter&quot;,
      &quot;level&quot;: &quot;ERROR&quot;,
      &quot;appender&quot;: {
        &quot;type&quot;: &quot;dateFile&quot;,
        &quot;filename&quot;: &quot;log/err/err&quot;,
        &quot;alwaysIncludePattern&quot;: true,
        &quot;pattern&quot;: &quot;-yyMMdd.log&quot;
      }
    },
    {
      &quot;type&quot;: &quot;dateFile&quot;,
      &quot;filename&quot;: &quot;log/hour/hour&quot;,
      &quot;alwaysIncludePattern&quot;: true,
      &quot;pattern&quot;: &quot;-yyMMddhh.log&quot;
    },
    {
      &quot;category&quot;: &quot;http&quot;,
      &quot;type&quot;: &quot;dateFile&quot;,
      &quot;filename&quot;: &quot;log/http/http&quot;,
      &quot;alwaysIncludePattern&quot;: true,
      &quot;pattern&quot;: &quot;-yyMMdd.log&quot;
    }
  ],

  &quot;replaceConsole&quot;: true,
  &quot;levelOpt&quot;: &quot;ALL TRACE DEBUG INFO WARN ERROR FATAL OFF&quot;,
  &quot;levels&quot;: {
    &quot;http&quot;: &quot;ALL&quot;,
    &quot;[all]&quot;: &quot;INFO&quot;
  }
}
</code></pre>
<h3 id="-">实际代码</h3>
<pre><code class="lang-js">// 引用 第三方日志库，需安装好
import log4js from &#39;log4js&#39;;
/*
 * 创建日志目录
 */
try {
  if (!fs.existsSync(&#39;./log&#39;))
    fs.mkdirSync(&#39;./log&#39;);

  if (!fs.existsSync(&#39;./log/err&#39;))
    fs.mkdirSync(&#39;./log/err&#39;);

  if (!fs.existsSync(&#39;./log/hour&#39;))
    fs.mkdirSync(&#39;./log/hour&#39;);

  if (!fs.existsSync(&#39;./log/http&#39;))
    fs.mkdirSync(&#39;./log/http&#39;);
} catch (e) {
  console.error(&#39;Create log directory exp: %s&#39;, e.message);
}

// 获得日志输出，找不到index，则匹配所有没有命名的全局输出
const log = log4js.getLogger(&#39;index&#39;);
// 日志配置加载，每隔 300秒检查设置变更！
log4js.configure(&#39;./config/log4js.json&#39;, { reloadSecs: 300 });

// 测试
console.log(&#39;start log...&#39;); // 等同于 info
log.trace(&#39;Entering cheese testing&#39;);
log.debug(&#39;Got cheese.&#39;);
log.info(&#39;Cheese is Gouda.&#39;);
log.warn(&#39;Cheese is quite smelly.&#39;);
log.error(&#39;Cheese is too ripe!&#39;);
log.fatal(&#39;Cheese was breeding ground for listeria.&#39;);
</code></pre>
<p><div id='静态文件服务'/>  </p>
<h2 id="-">静态文件服务</h2>
<ul>
<li><a href="https://github.com/koajs/send">koa-send</a> tj参与的文件发送基本库
支持自动压缩等，代码不多，功能强大</li>
<li><a href="https://github.com/koajs/static">koa-static</a> tj参与的文件服务中间件</li>
<li><a href="https://github.com/pillarjs/send">send</a> tj参与的早期文件发送基本库
代码很多，express 使用的基本库</li>
<li><p><a href="https://github.com/expressjs/serve-static">serve-static</a> express常用的文件服务中间件
封装到 koa 2.0 代码示例:</p>
<pre><code class="lang-js">import serveStatic from &#39;serve-static&#39;;
function wrapServeStatic(serve) {
  return ctx =&gt; {
    const urlBackup = ctx.req.url;
    ctx.req.url = &#39;/&#39; + (ctx.params.pathname || &#39;&#39;);
    return new Promise(resolve =&gt; {
      serve(ctx.req, ctx.res, resolve);
    }).then(() =&gt; {
      ctx.req.url = urlBackup;
    });
  };
}

// koa-serve-static 封装的
function serveStatic(root, options) {
  const fn = serveStatic(root, options);
  return (ctx, next) =&gt; {
    return new Promise((resolve, reject) =&gt; {
      // hacked statusCode
      if (ctx.status === 404) ctx.status = 200
      // unnecessary response by koa
      ctx.respond = false
      // 404, serve-static forward non-404 errors
      // force throw error
      fn(ctx.req, ctx.res, reject)
    })
  }
}
</code></pre>
</li>
<li><p>封装 koa-send示例，测试通过 </p>
<pre><code class="lang-js">import Koa from &quot;koa&quot;;
const app = new Koa();
import send from &quot;koa-send&quot;;

const resolve = require(&#39;path&#39;).resolve;
const assert = require(&#39;assert&#39;);

function staticFile(root, opts) {
  opts = opts || {};

  assert(root, &#39;root directory is required to serve files&#39;);

  // options
  //debug(&#39;static &quot;%s&quot; %j&#39;, root, opts);
  opts.root = resolve(root);

  if (opts.index !== false)
    opts.index = opts.index || &#39;index.html&#39;;

  return async (ctx, next)=&gt;{
    console.log(ctx.url, ctx.path, opts.root, ctx.request.body);

    if (ctx.method == &#39;HEAD&#39; || ctx.method == &#39;GET&#39;) {
      if (await send(ctx, ctx.path, opts)) return;
    }
    await next();
  };
}

app.use(staticFile(&#39;./public&#39;));
</code></pre>
</li>
</ul>
<p><div id='路由'/> </p>
<h2 id="-">路由</h2>
<ul>
<li><a href="https://github.com/alexmingoia/koa-router">koa-router</a> 人气最高的路由中间件</li>
<li><p>Express风格， 使用 <code>app.get</code>, <code>app.put</code>, <code>app.post</code>, 等. </p>
<pre><code class="lang-js">.get|put|post|patch|delete|del|all(path, middleware, [...]) ⇒ Router

router
  .get(&#39;/&#39;, next =&gt; {
    this.body = &#39;Hello World!&#39;;
  })
  .post(&#39;/users&#39;, next =&gt; {
    // ...
  })
  .put(&#39;/users/:id&#39;, next =&gt; {
    // ...
  })
  .del(&#39;/users/:id&#39;, next =&gt; {
    // ...
  });
</code></pre>
</li>
<li>匹配具体路径的中间件，用于路由集合：<code>router.routes ⇒ function</code></li>
<li>匹配路由参数：<code>.param(param, middleware) ⇒ Router</code></li>
<li>匹配所有操作：<code>.all([path], middleware, [...]) ⇒ Router</code></li>
<li><p>前置中间件处理</p>
<pre><code class="lang-js">语法：.use([path], middleware, [...]) ⇒ Router

示例：
// 路由处理之前，执行的中间件
router.use(session(), authorize());
// 符合/user路径时，执行用户授权检查中间件
router.use(&#39;/user&#39;, userAuth());
// 符合/user路径时，执行子路由匹配！
router.use(&#39;/user&#39;, userRouter.routes());
</code></pre>
</li>
<li><p>重定向    </p>
<pre><code class="lang-js">outer.redirect(source, destination, code) ⇒ Router  
router.redirect(&#39;/login&#39;, &#39;sign-in&#39;);
</code></pre>
</li>
<li><p>多重路由：对一个路径，多个顺连的处理函数</p>
<pre><code class="lang-js">router.get(
  &#39;/users/:id&#39;,
  (ctx, next) =&gt; {
    ctx.user = await User.findOne(this.params.id);
    await next();
  },
  ctx =&gt; {
    console.log(ctx.user);
    // =&gt; { id: 17, name: &quot;Alex&quot; }
  }
);
</code></pre>
</li>
<li><p>路由嵌套：路由处理可以是另一个子路由，注意子路由是基于父路由的！</p>
<pre><code class="lang-js">userRt.post(&#39;/reg&#39;, next =&gt; {...}); // responds to &quot;/user/reg&quot;
userRt.get(&#39;/get&#39;, next =&gt; {...});  // responds to &quot;/user/get&quot;
forums.use(&#39;/user&#39;, userRt.routes(), posts.allowedMethods());
</code></pre>
</li>
<li>ES7 async/await 支持.</li>
<li>Named URL parameters.</li>
<li>Named routes with URL generation.</li>
<li>Responds to <code>OPTIONS</code> requests with allowed methods.</li>
<li>支持 <code>405 Method Not Allowed</code> and <code>501 Not Implemented</code>.</li>
<li>Multiple routers.</li>
</ul>
<h3 id="-">创建路由</h3>
<p>new Router([opts])
| Param | Type | Description |
| --- | --- | --- |
| [opts] | <code>Object</code> |  |
| [opts.prefix] | <code>String</code> | prefix router paths |</p>
<h3 id="-">示例代码</h3>
<p>app.js 中加载路由中间件</p>
<pre><code class="lang-js">// 加载路由中间件,处理路由匹配,无法匹配的,回到静态文件处理,静态文件找不到的,返回不存在错误!
app.use(router.routes());
</code></pre>
<p>route 目录下面增加 index.js文件：</p>
<pre><code class="lang-js">import Router from &#39;koa-router&#39;;

const rt = new Router();

// 首页
rt.get(&#39;/&#39;, ctx =&gt; {
  ctx.render( &#39;h1 Hello, #{name}！&#39;, { name: &#39;Route!&#39; }, { fromString: true }, false);
});

export default rt;
</code></pre>
<p>访问 <a href="http://localhost:3000/">http://localhost:3000/</a> 就能看到 Hello Route！</p>
<h3 id="-">增加子路由</h3>
<p>在大型系统中，一般会将路由分解到不同的文件中，而不是集中写在一个总路由中。</p>
<p>修改  route/index.js</p>
<pre><code class="lang-js">import userRouter from &#39;./user&#39;;

// 用户子路由处理
rt.use(&#39;/user&#39;, userRouter.routes());
</code></pre>
<p>增加 route/user.js</p>
<pre><code class="lang-js">import Router from &#39;koa-router&#39;;
const log = require(&#39;log4js&#39;).getLogger(&#39;route/user&#39;);

const rt = new Router();

// 返回用户注册页面
rt.get(&#39;/reg&#39;, ctx =&gt; {
  log.warn(&#39;enter user/reg page.&#39;);
  ctx.render(&#39;h1 Hello#{name}!&#39;, { name: &#39;user reg！&#39; }, { fromString: true }, false);
});

export default rt;
</code></pre>
<p>访问 <a href="http://localhost:3000/user/reg">http://localhost:3000/user/reg</a> 就能看到 Hello user reg！</p>
<p><div id='jade模板'/> </p>
<h2 id="jade-">jade模板</h2>
<blockquote>
<p>jade 已经更名为pug，不是解析最快（毫秒差距无足轻重），但是是最简洁、层次最清晰的模板   </p>
</blockquote>
<ul>
<li><a href="https://github.com/tj/consolidate.js">consolidate</a> tj写的几十种模板库！</li>
<li><a href="https://github.com/koajs/static">koa-react-view</a> react模板库</li>
<li><a href="https://github.com/queckezz/koa-views">koa-views</a> 封装了consolidate，支持 koa 2</li>
<li><a href="https://github.com/pugjs/pug">pug</a> jade 更名为 pug</li>
<li><a href="https://github.com/chrisyip/koa-pug">koa-pug</a> pug koa的封装</li>
<li><a href="http://jade-lang.com">jade官网</a></li>
<li><a href="https://github.com/donpark/html2jade">html2jade 转换代码库，用于程序内部使用</a></li>
<li><a href="http://html2jade.org">html2jade 网页在线转换（推荐）</a></li>
<li><a href="http://jumplink.github.io/jade2html2jade/">jade2html2jade双向转换（中文会自动转码）</a> </li>
<li><p>示例代码：</p>
<pre><code class="lang-js">import Koa from &#39;koa&#39;;
import Pug from &#39;koa-pug&#39;;

const app = new Koa();

// jade 模板
const pug = new Pug({
  app, // equals to pug.use(app) and app.use(pug.middleware)
  viewPath: &#39;./views&#39;,
  debug: process.env.NODE_ENV === &#39;development&#39; });

pug.locals.title = &#39;Koa Demo&#39;;

app.use(ctx =&gt; {
  ctx.render( &#39;h1 Hello, #{title} #{name}&#39;, { name: &#39;Pug&#39; }, { fromString: true }, false);
});

console.log(&#39;koa start on port 3003&#39;);
app.listen(3003);
</code></pre>
</li>
</ul>
<p><div id='参数存放'/> </p>
<h2 id="-">参数设置</h2>
<p>任何程序都需要一些参数设置文件来设置参数，程序运行时，能根据参数工作，这样在环境等变化时，就不需要修改代码，只需修改参数配置即可。  </p>
<h3 id="-">参数存放</h3>
<ul>
<li>参数一般分为两种，一种是存在文件里面，一种是存在数据库里面。  </li>
<li>存在文件里面，程序启动时加载，运行过程中，文件变化，需重启程序。  <ul>
<li>文件配置一般统一存放在 专门的 config 目录，统一管理。  </li>
<li>参数文件可使用 json 或 js文件，json是纯数据，不支持 备注，而 js支持备注，推荐 使用 js 作为参数配置文件</li>
<li>一般都会有 app.js，作为应用基本配置文件，请注意，这个文件在 config目录下面，不要与 src、lib、bin下面的 app.就是搞混了。</li>
</ul>
</li>
<li>存在数据库中，需要编写程序，实现自动加载、更新<ul>
<li>由于是自己的代码控制，在参数变化时，设置刷新标记，在不影响业务时，实现参数重新加载。  </li>
<li>一般会吧常用参数存放在内存中，触发或自动定时更新。</li>
<li>数据库参数通过远程可修改，而文件参数需登录到服务器才能修改</li>
<li>建议尽量使用数据库方式存储参数，一些隐秘的、基本的、不经常变化的存在文件里面。</li>
<li>数据库一般有个参数字典表，可设置为 code 名称，表示 参数代码。</li>
</ul>
</li>
</ul>
<h3 id="config-app-js-">config/app.js 参数文件举例</h3>
<pre><code class="lang-js">export default {
  app: {
    host: &#39;paiapp.com&#39;, // 域名 或 ip地址，登记时需要返回给登记者
    port: 3000, // 4502-4534  silverlight
    debug: true    // 调试环境还是生产环境
  },
  theme: {
    name: &#39;default&#39;
  },
  session: {
    secret: &#39;546gsfqwerqe4tgfghfghdfujtrt&#39;,
    expires: 1200, // 服务端session有效期，单位秒，空闲超时强制清除，客户端最好5分钟一次登记！超时20分钟不活动，判断为离线，个人聊天时无需精确状态消息，在close中不广播离线状态，在这里广播 离线状态？
    checktime: 60  // 多长时间 检查一次，单位秒
  },
  log: {
    level: &#39;ALL&#39; /* OFF FATAL ERROR WARN INFO DEBUG TRACE ALL */
  },
  // mongodb 数据库
  db: {
    conn: &#39;mongodb://localhost/pai&#39;,
    poolSize: 5
  },
  // 缓存服务器
  redis: { 
    host: &#39;localhost&#39;,
    port: 6379, // 6379在是手机按键上MERZ对应的号码，而MERZ取自意大利歌女Alessia Merz的名字
    password: &#39;&#39;
  },
  upfile: {
    maxSize: 20 // 上传最大文件大小，单位兆数，默认10兆，大约500万条政策数据
  }
};
</code></pre>
<h3 id="-">配置文件使用示例</h3>
<pre><code class="lang-js">import cfg from &#39;../../config/app.js&#39;;

// 缺省连接池为 5个
const conn = mongoose.createConnection(
  cfg.db.conn,
  {server: {poolSize: cfg.db.poolSize}}
);
</code></pre>
<h2 id="mongodb-">MongoDB 数据库</h2>
<blockquote>
<p>Mongoose 是 MongoDB数据库node.js下的ODM(Object Document Mapper，对象文档映射)。<br>以对象方式，提供数据校验、关联、连接池等各种更高级功能。</p>
</blockquote>
<ul>
<li>安装：npm i mongoose -S</li>
<li>安装原生驱动：npm install mongodb --save</li>
<li>原生驱动是基于字符串方式的文档模型操作，灵活但是容易出错，在密集写入场景可使用。</li>
<li>mongoose 在原生驱动上做了封装，密集操作时，性能上比原生操作慢一点点，但更健壮，推荐在常规场景使用。</li>
<li>mongoose详细操作请参见专门的<a href="https://github.com/nydl/devnote/blob/master/mongoose.md">mongoose 使用说明</a></li>
<li>mongoDB详细说明请参见专门的<a href="https://github.com/nydl/devnote/blob/master/mongoDB.md">mongoDB 使用说明</a></li>
</ul>
<h3 id="-">数据模型定义</h3>
<p>model中增加了两个文件：db.js 和 user.js，一个是管理数据库连接，一个是管理用户数据。</p>
<p>db.js 封装了数据库连接的创建与获取，代码如下：</p>
<pre><code class="lang-js">const mongoose = require(&#39;mongoose&#39;);
import cfg from &#39;../../config/app.js&#39;;

// 缺省连接池为 5个
const conn = mongoose.createConnection(
  cfg.db.conn,
  {server: {poolSize: cfg.db.poolSize}}
);

conn.on(&#39;error&#39;, err =&gt; console.log(err));

export default conn;
</code></pre>
<p>user.js 定义了数据库中 users表（mongodb 称之为文档，不叫表，我们习惯还是叫表）的字段，注意，mongodb里面存储的表名会自动加“s”，比如 user，数据库里面就是 users。</p>
<pre><code class="lang-js"> const mongoose = require(&#39;mongoose&#39;);
const schema = mongoose.Schema;
const ObjectId = schema.ObjectId;

/**
 * User 模型定义
 * name: { type: String, index: true }
 */
const userSchema = schema({
  // pai号，目前暂定为 8 位
  pid: { type: String, required: true, unique: true }, // pai号，必须有值，必须唯一，重复无法插入
  mobile: {type: String, required: true, unique: true}, // 手机号，企业、群组没有手机，可设置为固定电话，或者创办人手机前加 G，避免手机重复
  email: {type: String, sparse: true, unique: true},  // 邮箱，稀疏索引，支持多个无值都能插入，有值必须唯一，不能填空，不能null

  unitId: Number,   // 单位 Id，自增长索引，可选
  unitOid: ObjectId, // 单位ObjectId
  unitPid: String,  // 单位 pid

  rid: {type: String},     // 第三方系统id号，如 马蜂窝的 uid
  from: {type: String},    // 用户来源，比如 mfw 表示马蜂窝

  name: {type: String, index: true}, // 真实姓名，用于朋友之间显示的名称
  alias: String,    // 别名，类似微信的备注名称，可修改好友的名称为别名
  nick: String,     // 昵称，用于陌生人显示的名称
  mark: String,     // 个性签名，微博、QQ、微信在名字后面显示 sign api接口的 sign 冲突
  sex: String,      // 性别，男、女，企业、群组无
  password: String, // md5 编码

  // 类别 0：待定，作为普通个人用户处理 1：普通个人 2：企业用户 3：群组，群组依附于会员存在 4：公众，包括 个人、媒体、企业等需社会化营销的用户
  type: Number,     // 普通、企业 的分享，是否可向公众 转发、关注，从而产生粉丝，类似轻微博，粉丝只能看到用户通讯录之外的粉丝和关注，对通讯录进行保护
  verify: Boolean,  // 实名认证、验证，坐过飞机的，自动通过认证
  rating: Number,   // 等级  星级  可建立梦幻的、值得骄傲的，代表身份的等级制，包括 分享数量、粉丝数量、转发数量等
  tag: [],          // 标签，如 明星,记者,机票出票

  imei: String,     // 手机设备串号，可用于绑定
  ip: String,       // 当前计算机IP地址，一般是变化的
  pcid: String,     // 计算机设备号，如 cpuid，硬盘id，网卡mac地址等用于标识当前计算机设备，用于绑定登录

  icon: String,     // 用户图标
  url: String,      // 用户工作网址
  host: String,     // 登记、归属主机
  unitNo: String,   // 邀请码，单位号，用于识别所属主机及单位，用于所动该用户单位
  status: Number,   // 用户状态 0：新用户待定 1：确认 2：复核 3：删除 4：过期

  // pai状态 0：未登记（未登录） 离线 1：登记 上线（一般用于登录状态判断） 2:在线（普通空闲状态） 3：忙碌 4：免打扰 5：隐身 6：离开
  // 离线后第一次登录，状态填1，成功登录后，根据上次状态判断，如上次为隐身，则进入隐身，否则 进入 2 在线状态，这样方便统计用户登录情况
  // 状态说明，如 打字中，就餐等。。。
  // 工作状态 0:缺席（未上岗） 1：上岗（开始上班，类似登录） 2:就绪（可分派业务） 3:忙碌（电话会话） 4:暂停（不分派业务） 5：外呼 6：离席
  // 工作状态说明，如 离席 会议、就餐等。。。state: {type: Array, default: [0, &#39;&#39;, 0, &#39;&#39;]},

  friend: [],      // 好友，安装了pai
  contact: [],      // 联系人，一个人一般都有100-1000的联系人，这些都是需要转化pai用户的目标

  group: [],       // 加入的群组
  member: [],       // 群组成员，对群组用户有效

  // 订阅别人，关注别人，用于主动获取订阅者、关注者状态
  sub: {
    sta: [],       // 状态订阅者，有时只需要状态
    all: [],       // 所有关注，微博的 关注
    msg: [],       // 消息分享订阅者
    pic: []       // 相册订阅者
  },
  // 被别人订阅，被关注
  subed: {
    sta: [],       // 状态订阅者
    all: [],       // 所有订阅者，微博的粉丝
    msg: [],       // 分享订阅者
    pic: []       // 相册订阅者
  },

  desc: String,     // 个人简介
  notes: String,    // 备注
  remark: String,   // 内部备注，内部可见
  sellId: ObjectId, // 推荐者、销售员UserId
  sellPid: String,   // 销售员pid
  seller: String,   // 销售员名称

  serviceId: ObjectId,  // 客服Id
  servicePid: String,  // 客服名称
  servicer: String,  // 客服名称

  adminId: ObjectId, // 管理人id
  adminPid: String,
  admin: String,    // 管理人名称，公众、群组用户 需设置管理员，方便联系

  addUserId: ObjectId, // 创建用户, ObjectId 唯一，不可被修改！
  addPid: String, // 创建用户
  addUser: String, // 创建用户
  addTime: {type: Date, default: Date.now}, // 创建时间

  upUserId: ObjectId, // 更新用户
  upPid: String, // 更新用户
  upUser: String, // 更新用户
  upTime: Date  // 更新时间
});

// 通过 require db.conn 来返回指定的数据库 User 模型
export default function userM(conn) {
  // 定义 model
  mongoose.model(&#39;user&#39;, userSchema);
  // 获取 model
  return (conn || mongoose).model(&#39;user&#39;);
}
</code></pre>
<h3 id="-">写入数据</h3>
<p>我们来写个测试代码，向 数据库中的 users表 插入一条记录：</p>
<pre><code class="lang-js">/**
 * Data generation
 */
import conn from &#39;./db&#39;;
function add(pid) {
  const UserM = userM(conn);

  UserM.create({
    pid: pid,
    name: &#39;test&#39;,
    showName: &#39;test&#39;,
    Sex: &#39;男&#39;,
    mobile: &#39;13900000001&#39;,
    unitNo: &#39;0086.11112222&#39;,
    status: 1,
    upTime: new Date(&#39;2016/05/16 23:00:00&#39;),
  }, err =&gt; {
    if (err) {
      console.error(err.message);
    } else {
      console.log(&#39;create data!&#39;);
    }
  });
}

/**
 * Test
 */
add(&#39;88880003&#39;);
</code></pre>
<h3 id="-">查看写入的数据</h3>
<ul>
<li>启动 mongodb 数据库服务，如何启动，请参见 mongoDB文档</li>
<li>打开命令行，使用 mongo 来查看数据，执行指令如下：<pre><code class="lang-js">mongo // mongodb 客户端工具
use pai // 切换数据库
db.users.find() // 列出所有users表记录
db.users.find().count() // users表记录数量
</code></pre>
</li>
</ul>
<h2 id="-">用户逻辑封装</h2>
<p>用户数据库表（mongodb 称之为文档，不叫表，我们习惯还是叫表）定义完毕，并且通过 mongoose 生成了 对象模型 model，接下来我们需要封装一个用户类，将所有用户操作数据库以及用户相关逻辑存放在这个用户类中，方便统一管理。</p>
<p>为了易懂，这个文件还是取名为 user.js，目录为 logic，注意，user.js 已经出现了3次，分别在 model、route、logic，分别代表 数据模型、业务逻辑、路由交互。</p>
<h3 id="-babel-">使用 babel 支持 类静态属性</h3>
<p>类封装，需要用到 类的静态属性、静态方法等，静态方法在 es5/6中已经得到支持，静态属性还没有，需要安装 babel 插件！</p>
<p>安装：</p>
<pre><code class="lang-js">npm i -D babel-plugin-syntax-class-properties
npm i -D babel-plugin-transform-class-properties
</code></pre>
<p>启用：在 .babelrc中的 &quot;plugins&quot; 增加</p>
<pre><code class="lang-js">&quot;syntax-class-properties&quot;,ı
&quot;transform-class-properties&quot;
</code></pre>
<h3 id="-">增加子路由</h3>
<p>// 注册用户，post
rt.post(&#39;/api/reg&#39;, user.reg);</p>
<ul>
<li>实现用户注册接口</li>
</ul>
<h2 id="-">部署运行</h2>
<ul>
<li><a href="https://github.com/remy/nodemon/">nodemon</a>
开发时使用，自动监控文件变化，自动重新加载
c:\program&gt;nodemon d:/js/node/app.js 
监视的是c:\program这个启动目录，而不是d:/js/node这个目录</li>
<li><a href="https://github.com/Unitech/pm2">pm2</a>
生产时使用</li>
<li><a href="https://github.com/petruisfan/node-supervisor">supervisor</a>
逐渐被 nodemon 替代</li>
<li><a href="https://github.com/foreverjs/forever">forever</a> 
逐渐被 pm2 替换</li>
</ul>
<h3 id="-">无中断重启</h3>
<p>由于node是单线程工作（多线程可实现线程的单独加载），运行时所有代码已经加载到通用内存，修改代码后，需关闭服务，重新加载新代码执行。<br>不仅仅是代码，包括一些配置参数修改了，也需重启服务，所以建议参数不要放入config文件，统一放到数据库中，通过后台刷新标记进行动态加载。</p>
<p>重启服务可能会影响正在运行的业务，比如一个用户正在支付，重启就打断了支付流程。<br>结合 nginx 反向代理，部署多套 node，可以实现业务无中断重启。<br>该原理就是重启前，与 nginx协商好，停止分配连接，等当前连接处理完毕，即可重启。重启后，再通知 nginx 分发连接，进行处理。
理论上需要部署多台 node 服务器，如果只有一台，可以在一台上部署两套 node，两个端口，分别使用 一半的 cpu 来处理。</p>
<p>pm2 等工具实际上多进程运行工具，如果每个进程使用一套内存代码，理论上可以实现类似的功能，就是多个进程在处理完当前请求后，自动加载新的代码，这样一个个进程单独更新，实现无终端重启。</p>
<p>我们用 express.js 服务来举例，koa 也是类似的！</p>
<pre><code class="lang-js">app = express.createServer() 
... 
app.listen(31337)
</code></pre>
<p>对服务的做如下调整（示意代码，真实代码待后续测试成功补上）：</p>
<pre><code class="lang-js">app = express.createServer() ... 
gracefullyClosing = false 
app.use (req, res, next) -&gt; 
return next() unless gracefullyClosing 
res.setHeader &quot;Connection&quot;, &quot;close&quot; 
res.send 502, &quot;Server is in the process of restarting&quot;

httpServer = app.listen(31337) 
process.on &#39;SIGTERM&#39;, -&gt; gracefullyClosing = true
</code></pre>
<p>nginx 反向代理服务设置如下（示意代码）：</p>
<pre><code class="lang-js">upstream silly_face_society_upstream {
  server 127.0.0.1:61337;
  server 127.0.0.1:61338;
  keepalive 64;
}
</code></pre>
<h2 id="-">运行测试</h2>
<p><code>$ make test</code></p>
<h2 id="-application-">应用（Application）</h2>
<p>一个 Koa Application（以下简称 app）由一系列中间件组成。按照编码顺序在栈内依次执行，从这个角度来看，Koa app 和其他中间件系统（比如 Ruby Rack 或者 Connect/Express ）没有什么太大差别，不过，从另一个层面来看，Koa 提供了一种基于底层中间件编写「语法糖」的设计思路，这让设计中间件变得更简单有趣。</p>
<p>在这些中间件中，有负责内容协商（content-negotation）、缓存控制（cache freshness）、反向代理（proxy support）与重定向等等功能的常用中间件（详见 中间件 章节），但如前所述， Koa 内核并不会打包这些中间件。</p>
<p>让我们先来看看 Koa 极其简单的 Hello World 应用程序：</p>
<h2 id="-cascading-">级联调用（Cascading）</h2>
<h3 id="-">级联概念</h3>
<p>Koa 中间件以一种非常传统的方式级联起来，类似 connect，你可能会非常熟悉这种写法，就像流水线一样，对web请求进行一级级处理。</p>
<p>在以往的 Node 开发中，由于其异步性，导致每个执行结果都要使用回调，当一个结果需要等待另一个结果是，就会导致非常深的回调嵌套。<br>频繁使用回调不太便于展示复杂的代码逻辑，在 Koa 中，我们可以写出真正具有表现力的中间件。与 Connect 实现中间件的方法相对比，Koa 的做法不是简单的将控制权依次移交给一个又一个的中间件直到程序结束，Koa 执行代码的方式有点像回形针，用户请求通过中间件，遇到 await next() 关键字时，会被传递到下一个符合请求的路由（downstream），在 await next() 捕获不到下一个中间件时，逆序返回继续执行代码。</p>
<p>简单来说，就是将以前嵌套的回调函数，不再嵌套，直接在同一层编写，系统在 await 等待结果后继续执行后续代码，等同于回调，效果一样，只是语义更加自然，更加易懂。  </p>
<p>下边这Hello World 范例演示了这种平铺型级联模式。<br>一开始，用户的请求通过 x-response-time 中间件和 logging 中间件，这两个中间件记录了一些请求细节，然后「穿过」response 中间件一次，最终结束请求，返回 「Hello World」。</p>
<p>当程序运行到 await next() 时，代码流会暂停执行这个中间件的剩余代码，转而切换到下一个被定义的中间件执行代码，等待对方交回控制权，这样切换控制权的方式，被称为 downstream，当没有下一个中间件执行 downstream 的时候，代码将会逆序执行 upstream。</p>
<h3 id="-">代码示例：</h3>
<pre><code class="lang-js">/**
 * Created by way on 16/5/10.
 */

const Koa = require(&#39;koa&#39;);
const app = new Koa();

async function delay(ms) {
  return new Promise(resolve =&gt; {
    setTimeout(resolve, ms);
  });
}

// uses async arrow functions
app.use(async (ctx, next) =&gt; {
  try {
/*
    if (__DEV__) {
      console.log(ctx.url, ctx.request.body);
    }
*/
    console.log(&#39;(1) 进入路由&#39;);
    const start = new Date();
    await next(); // 执行并等待下一个中间件
    console.log(&#39;(5) 再次进入 x-response-time 中间件，记录2次通过此中间件的时间&#39;);
    const ms = new Date() - start;
    ctx.set(&#39;X-Response-Time&#39;, ms + &#39;ms&#39;);
    console.log(`enter: ${ctx.method} ${ctx.url} - ${ms}ms`);
    console.log(&#39;(6) 返回 ctx.body&#39;);
  } catch (err) {
    ctx.body = { message: err.message };
    ctx.status = err.status || 500;
  }
});


// logger
app.use(async (ctx,next) =&gt; {
  console.log(&#39;(2) 进入 logger 中间件&#39;);
  const start = new Date();
  await next(); // 执行并等待下一个中间件,如果不等待结果,直接 在最后调用 next()即可!
  console.log(&#39;(4) 再次进入 logger 中间件，记录2次通过此中间件的时间&#39;);
  const ms = new Date() - start;
  console.log(`logger: ${ctx.method} ${ctx.url} - ${ms}ms`);
});


// response
app.use(async ctx =&gt; {
  console.log(&#39;(3) 进入 response 中间件，没有下一个next,开始返回&#39;);
  //const user = await User.getById(ctx.session.userid); // await instead of yield
  await delay(200);
  ctx.body = `Hello Koa!`;
});

app.listen(3000);
</code></pre>
<h3 id="-">示例解析</h3>
<p>在上方的范例代码中，中间件以此被执行的顺序已经在注释中标记出来。可以尝试运行一下这个范例，并打印记录下各个环节的输出与耗时。<br>「级联」这个词许多人也许在 CSS 中听说过，如果你不能理解为什么在这里使用这个词，可以将这种路由结构想象成 LESS 的继承嵌套书写方式：</p>
<pre><code class="lang-js">.middleware1 {
  // (1) do some stuff
  .middleware2 {
    // (2) do some other stuff
    .middleware3 {
      // (3) NO next await !
      // ctx.body = &#39;hello world&#39;
    }
    // (4) do some other stuff later
  }
  // (5) do some stuff lastest and return
}
</code></pre>
<p>上方的伪代码中标注了中间件的执行顺序，看起来是不是有点像 ruby 执行代码块（block）时 yield 的表现了？也许这能帮助你更好的理解 koa 运作的方式。</p>
<h3 id="-">级联调用形象示意图</h3>
<p><img src="https://raw.githubusercontent.com/nydl/devnote/master/img/koa-onion.png" alt="onion.png"></p>
<h2 id="-settings-">应用配置（Settings）</h2>
<p>应用的配置是 app 实例的属性。目前来说，Koa 的配置项如下：</p>
<ul>
<li><strong>app.name</strong> 应用名称</li>
<li><strong>app.env</strong> 执行环境，默认是 NODE_ENV 或者 development 字符串</li>
<li><strong>app.proxy</strong> 决定了什么 proxy header 参数会被加到信任列表中</li>
<li><strong>app.subdomainOffset</strong> 被忽略的 .subdomains 列表</li>
<li><strong>app.jsonSpaces</strong> 默认的 JSON 响应空间</li>
<li><strong>app.outputErrors</strong> 是否输出错误堆栈 err.stack 到 stderr [当执行环境是 &quot;test&quot; 的时候为 false]</li>
</ul>
<h2 id="-">代码解析</h2>
<h3 id="app-listen-">app.listen(...)</h3>
<p>用于启动一个服务的快捷方法，以下范例代码在 3000 端口启动了一个空服务：</p>
<pre><code class="lang-js">var koa = require(&#39;koa&#39;);
var app = koa();
app.listen(3000);

app.listen 是 http.createServer 的简单包装，它实际上这样运行：

http.createServer(app.callback()).listen(3000);
</code></pre>
<p>如果有需要，你可以在多个端口上启动一个 app，比如同时支持 HTTP 和 HTTPS：</p>
<pre><code class="lang-js">http.createServer(app.callback()).listen(3000);  
http.createServer(app.callback()).listen(3001);
</code></pre>
<h3 id="app-callback-">app.callback()</h3>
<p>返回一个可被 http.createServer() 接受的程序实例，也可以将这个返回函数挂载在一个 Connect/Express 应用中。</p>
<h3 id="app-use-function-">app.use(function)</h3>
<p>将给定的 function 当做中间件加载到应用中。</p>
<h3 id="app-keys-">app.keys=</h3>
<p>设置一个签名 Cookie 的密钥。这些参数会被传递给 KeyGrip 如果你想自己生成一个实例，也可以这样：</p>
<pre><code class="lang-js">app.keys = [&#39;im a newer secret&#39;, &#39;i like turtle&#39;];  
app.keys = new KeyGrip([&#39;im a newer secret&#39;, &#39;i like turtle&#39;], &#39;sha256&#39;);
</code></pre>
<p>注意，签名密钥只在配置项 signed 参数为真是才会生效：</p>
<pre><code class="lang-js">this.cookies.set(&#39;name&#39;, &#39;tobi&#39;, { signed: true });
</code></pre>
<h2 id="-error-handling-">错误处理（Error Handling）</h2>
<p>除非应用执行环境被配置为 &quot;test&quot;，Koa 都将会将所有错误信息输出到 stderr，和 Connect 一样，你可以自己定义一个「错误事件」来监听 Koa app 中发生的错误：</p>
<pre><code class="lang-js">app.on(&#39;error&#39;, function(err){  
  log.error(&#39;server error&#39;, err);
});
</code></pre>
<p>当任何 req 或者 res 中出现的错误无法被回应到客户端时，Koa 会在第二个参数传入这个错误的上下文：</p>
<pre><code class="lang-js">app.on(&#39;error&#39;, function(err, ctx){  
  log.error(&#39;server error&#39;, err, ctx);
});
</code></pre>
<p>如果任何错误有可能被回应到客户端，比如当没有新数据写入 socket 时，Koa 会默认返回一个 500 错误，并抛出一个 app 级别的错误到日志处理中间件中。</p>
<h2 id="-">中间件</h2>
<ul>
<li>koa2 使用了 Promise模式，全面支持 async、await，对原 generator不再支持。</li>
<li>部分中间件升级可支持 2.0，安装中间件时要注意看，是否支持 koa2</li>
<li>否则需要自己就行封装，可对express中间件进行封装，示例如下：
<a href="https://github.com/ucms/ucms-plugin-file-store/blob/master/src/index.js">https://github.com/ucms/ucms-plugin-file-store/blob/master/src/index.js</a></li>
<li><p>或者使用koa提供的 koa-convert 对之前的koa中间件进行封装调用</p>
<ul>
<li>安装 <code>$ npm install koa-convert</code></li>
<li>使用示例：</li>
</ul>
<pre><code class="lang-js">const Koa = require(&#39;koa&#39;) // koa v2.x
const convert = require(&#39;koa-convert&#39;)
const app = new Koa()

app.use(modernMiddleware)

app.use(convert(legacyMiddleware))

app.use(convert.compose(legacyMiddleware, modernMiddleware))

function * legacyMiddleware (next) {
  // before
  yield next
  // after
}

function modernMiddleware (ctx, next) {
  // before
  return next().then(() =&gt; {
    // after
  })
}
</code></pre>
</li>
</ul>
